package csm.sql;

import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import csm.common.SMException;
import csm.common.TypeConv;
import csm.common.configuration.ConfigHandler;

/**
 * 
 * @author seungmun
 * @since 2006.1.18.
 * @version
 * @see
 */
public class DBHelper
{
	private String connectionName = "default";

	private Connection cn = null;
	private String sql = null;
	private ArrayList<ParameterSet> paramSet = null;
	private boolean externConnection = false;
	private int SCROLL_TYPE;
	private int CONCURRENCY_LEVEL;

	/**
	 * DBHelper의 기본 생성자
	 */
	public DBHelper()
	{
		this.connectionName = "default";
	}

	/**
	 * DBHelper의 생성자
	 * @param connectionName 커넥션을 얻어오는데 사용할 설정명
	 * @param sql
	 */
	protected DBHelper(String connectionName, String sql)
	{
		this.sql = sql;
		this.connectionName = connectionName;
	}

	/**
	 * DBHelper의 생성자
	 * 
	 * @param sql
	 */
	public DBHelper(String sql)
	{
		this.sql = sql;
		this.connectionName = "default";
	}

	/**
	 * 연결에 사용할 커넥션을 얻는데 사용하는 접속정보를 구분하는 이름을 가져옵니다.
	 * @return
	 */
	public String getConnectionName()
	{
		return connectionName;
	}

	/**
	 * 연결에 사용할 커넥션을 얻는데 사용하는 접속정보를 구분하는 이름을 설정합니다.
	 * @param connectionName
	 */
	public void setConnectionName(String connectionName)
	{
		this.connectionName = connectionName;
	}

	/**
	 * 실행할 SQL문을 설정한다.
	 * 
	 * @param sql
	 */
	public void setSql(String sql)
	{
		this.sql = sql;
	}

	/**
	 * 외부연결을 설정한다. <br>
	 * 외부연결을 사용하였을 경우에는 <U><B>반드시</B></U> 연결을 설정한 메서드 내에서 연결을 끊어야 한다.
	 * 
	 * @param cn
	 */
	public void setConnection(Connection cn)
	{
		if (cn != null) externConnection = true;
		this.cn = cn;
	}

	protected Connection getConnection() throws SQLException
	{
		if (externConnection) return cn;

		DBConfig dbc = DBConfig.getDBConfig(this.connectionName);
		try
		{
			SCROLL_TYPE = TypeConv.ToInt(dbc.getSettings("connection.scrollType"));
		}
		catch (Exception e)
		{
			SCROLL_TYPE = ResultSet.TYPE_SCROLL_INSENSITIVE;
		}
		try
		{
			CONCURRENCY_LEVEL = TypeConv.ToInt(dbc.getSettings("connection.concurrencyLevel"));
		}
		catch (Exception e)
		{
			CONCURRENCY_LEVEL = ResultSet.CONCUR_READ_ONLY;
		}
		return dbc.getConnection();
	}

	/**
	 * 파라메터들을 설정합니다.
	 * @param obj
	 * @param type
	 */
	public void appendParameter(Object obj, int type)
	{
		appendParameter(obj, type, false, false, true);
	}

	/**
	 * 파라메터들을 Output Parameter로 설정합니다.<br>
	 * <i>이 메서드는 executeProcedure를 호출할때만
	 * 씁니다.</i>
	 * 
	 * @param type
	 */
	public void appendOutputParameter(int type)
	{
		appendParameter(null, type, true, false, false);
	}

	/**
	 * 파라메터들을 input/output Parameter로 설정합니다.<br>
	 * <i>이 메서드는 executeProcedure를 호출할때만 씁니다.</i>
	 * 
	 * @param obj
	 * @param type
	 */
	public void appendInOutParameter(Object obj, int type)
	{
		appendParameter(obj, type, true, true, false);
	}

	/**
	 * 파라메터들을 설정합니다.
	 * 
	 * @param obj
	 * @param type
	 * @param forOutput
	 *            output으로 쓰일 값인지 설정한다. (executeProcedure메서드를 호출할 경우에만 쓴다.)<br>
	 *            <i>forOutput이 true인 경우에는 반드시 변수로 선언해서 대입하도록 한다.</i>
	 * @param forInOut
	 *            output으로 쓰일 값인지 설정한다. (executeProcedure메서드를 호출할 경우에만 쓴다.)<br>
	 *            <i>forInOut이 true인 경우에는 반드시 변수로 선언해서 대입하도록 한다.</i>
	 * @param forInputOnly
	 */
	private void appendParameter(Object obj, int type, boolean forOutput, boolean forInOut, boolean forInputOnly)
	{
		if (paramSet == null)
		{
			paramSet = new ArrayList<ParameterSet>();
		}
		
		if (forInputOnly)
			paramSet.add(new ParameterSet(this.connectionName, type, obj));
		else
			paramSet.add(new ParameterSet(this.connectionName, type, obj, forOutput, forInOut));
	}

	/**
	 * 지정된 SQL문을 수행하고 반영된 결과행수를 Return합니다.
	 * 
	 * @return 반영된 결과행수
	 * @throws SQLException
	 */
	public int execute() throws SQLException
	{
		PreparedStatement pstmt = null;
		try
		{
			cn = getConnection();
			pstmt = cn.prepareStatement(sql);
			appendToStatement(pstmt);
			int result = pstmt.executeUpdate();

			return result;
		}
		finally
		{
			if (pstmt != null) try
			{
				pstmt.close();
				pstmt = null;
			}
			catch (SQLException e)
			{
			}
			if (!externConnection)
			{
				try
				{
					if (cn != null)
					{
						cn.close();
						cn = null;
					}
				}
				catch (SQLException e)
				{
				}
			}
		}

	}

	/**
	 * PreparedStatement에 값을 추가한다.
	 * 
	 * @param pstmt
	 * @throws SQLException
	 */
	private void appendToStatement(PreparedStatement pstmt) throws SQLException
	{
		DBConfig dbc = DBConfig.getDBConfig(this.connectionName);

		int paramIndex = 0;
		if (paramSet != null)
		{
			for (ParameterSet ps : paramSet)
			{
				paramIndex++;
				if (ps.getValue() == null)
				{
					pstmt.setNull(paramIndex, ps.getColumnType());
					continue;
				}
				if (ps.getColumnType() == Types.LONGVARCHAR)
				{
					String columnValue = ps.getValue().toString();
					if (dbc.getDbms().equalsIgnoreCase("Informix"))
					{
						try
						{
							pstmt.setBytes(paramIndex, columnValue.getBytes("UTF-8"));
						}
						catch (UnsupportedEncodingException e)
						{
						}
					}
					else
					{
						StringReader reader = new StringReader(columnValue);
						pstmt.setCharacterStream(paramIndex, reader, columnValue.length());
					}
				}
				else
				{
					pstmt.setObject(paramIndex, ps.getValue(), ps.getColumnType());
				}
			}
		}
	}

	/**
	 * 쿼리를 실행하고 ResultSet 을 반환합니다.<br>
	 * 반환된 ResultSet 을 close 하면, 쿼리 실행에 사용된 ,Statement 나 Connection도 같이 Close 됩니다.<br>
	 * 단, setConnection 메소드를 통해 외부에서 전달된 커넥션을 사용한 경우는 제외됩니다.
	 * 
	 * @return ResultSet
	 * @throws Exception 
	 */
	public ResultSet executeQuery() throws Exception
	{
		Connection cn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			cn = getConnection();
			pstmt = cn.prepareStatement(sql, SCROLL_TYPE, CONCURRENCY_LEVEL);
			appendToStatement(pstmt);
			rs = pstmt.executeQuery();
			ClosableResultSet crs = new ClosableResultSet();
			crs.setConnection(cn);
			crs.setResultSet(rs);
			crs.setIsExternalConnection(externConnection);
			return crs;
		}
		catch (Exception ex)
		{
			System.err.println(getDebugString());
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
			if (pstmt != null)
			{
				try
				{
					pstmt.close();
					pstmt = null;
				}
				catch (SQLException e)
				{
				}
			}
			if (cn != null)
			{
				if (!externConnection)
				{
					try
					{
						if (cn != null)
						{
							cn.close();
							cn = null;
						}
					}
					catch (SQLException e)
					{
					}
				}
			}

			throw ex;
		}
	}

	/**
	 * 설정된 Procedure call을 한다.
	 * 
	 * @return
	 * @throws SQLException
	 */
	public ArrayList<ParameterSet> executeProcedure() throws SQLException
	{
		CallableStatement cstmt = null;
		try
		{
			cn = getConnection();
			cstmt = cn.prepareCall(sql);
			int paramIndex = 0;
			if (paramSet != null)
			{
				for (ParameterSet ps : paramSet)
				{
					paramIndex++;
					if (ps.isForInputOnly() || ps.isForInputOutput())
					{
						if (ps.getValue() == null)
						{
							cstmt.setNull(paramIndex, ps.getColumnType());
							continue;
						}
						cstmt.setObject(paramIndex, ps.getValue(), ps.getColumnType());
					}
					if (ps.isForOuptut() || ps.isForInputOutput())
					{
						cstmt.registerOutParameter(paramIndex, ps.getColumnType());
					}
				}
			}
			cstmt.executeUpdate();
			ArrayList<ParameterSet> arr = new ArrayList<ParameterSet>();
			paramIndex = 0;
			for (ParameterSet ps : paramSet)
			{
				paramIndex++;
				if (ps.isForInputOutput() || ps.isForOuptut())
				{
					switch (ps.getColumnType())
					{
						case Types.INTEGER:
							ps.setValue(cstmt.getInt(paramIndex));
							break;
						case Types.DATE:
							ps.setValue(new Date(cstmt.getDate(paramIndex).getTime()));
							break;
						default:
							ps.setValue(cstmt.getObject(paramIndex));
							break;
					}
					ps.setValue(cstmt.getObject(paramIndex));
					arr.add(ps);
				}
			}
			return arr;
		}
		catch (SQLException ex)
		{
			throw ex;
		}
		finally
		{
			if (cstmt != null) try
			{
				cstmt.close();
				cstmt = null;
			}
			catch (SQLException e)
			{
			}
			if (!externConnection)
			{
				try
				{
					if (cn != null)
					{
						cn.close();
						cn = null;
					}
				}
				catch (SQLException e)
				{
				}
			}

		}
	}

	/**
	 * 쿼리로 가져온 결과를 ArrayList로 돌려준다.
	 * 
	 * <pre>
	 * String sql = &quot;SELECT * FROM foo&quot;;
	 * DBHelper helper = new DBHelper(sql);
	 * ArrayList&lt;Foo&gt; arr = helper.getResultArrayList(Foo.class);
	 * for (Foo foo : arr)
	 * {
	 * 	System.out.println(foo.getCodeValue() + &quot;--&gt;&quot; + foo.getCount());
	 * }
	 * System.out.println(helper.isExist());
	 * </pre>
	 * 
	 * @param <T>
	 *            {@link ObjectMappedEntityBase}에서 파생된 클래스
	 * @param c
	 *            클래스 형태
	 * @return java.util.ArrayList<T>
	 * @throws SMException
	 */
	public <T extends ObjectMappedEntityBase> List<T> getResultList(Class<T> c) throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			T obj = null;
			ArrayList<T> arr = new ArrayList<T>();
			boolean result = true;
			while (result)
			{
				obj = c.newInstance();
				result = obj.bind(rs);
				if (result) arr.add(obj);
			}
			return arr;
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(), e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}
	}

	/**
	 * 설정한 쿼리를 실행하고, 결과가 존재하는지 여부를 가져옵니다.
	 * 
	 * @return
	 * @throws SMException
	 */
	public boolean isExist() throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			return (rs.next());
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(),e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}

	}

	/**
	 * 결과의 첫번째 row를 bind해준다.
	 * 
	 * @param obj
	 * @return
	 * @throws SMException
	 */
	public boolean bindFirstRow(ObjectMappedEntityBase obj) throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			return obj.bind(rs);
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(), e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}
	}

	/**
	 * 맨 처음 스칼라 값을 가져온다.<br>
	 * 
	 * @return
	 * @throws SMException
	 */
	public Object executeScalar() throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			if (rs.next()) return rs.getObject(1);
			return null;
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(), e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}
	}

	/**
	 * 맨 처음 String 스칼라 값을 가져온다.
	 * 
	 * @return
	 * @throws SMException
	 */
	public String executeString() throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			if (rs.next()) return rs.getString(1);
			return null;
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(), e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}
	}

	/**
	 * 맨 처음 Int 스칼라 값을 가져온다.<br>
	 * 
	 * @return
	 * @throws SMException
	 */
	public int executeInt() throws SMException
	{
		ResultSet rs = null;
		try
		{
			rs = executeQuery();
			if (rs.next()) return rs.getInt(1);
			return Integer.MIN_VALUE;
		}
		catch (Exception e)
		{
			throw new SMException(e.getMessage(), e);
		}
		finally
		{
			if (rs != null)
			{
				try
				{
					rs.close();
					rs = null;
				}
				catch (SQLException e)
				{
				}
			}
		}
	}

	/**
	 * 파라미터를 초기화한다.
	 */
	public void clearParameters()
	{
		paramSet.clear();
	}
	
	@Override
	protected void finalize() throws Throwable
	{
		if( paramSet != null )
		{
			paramSet.clear();
			paramSet = null;
		}
		
		super.finalize();
	}
	

	private String getDebugString()
	{
		StringBuffer errmsg = new StringBuffer();
		errmsg.append(String.format("%s \n", sql));
		if (paramSet != null)
		{
			errmsg.append("[Param");
			for (ParameterSet ps : paramSet)
			{
				if (ps.getValue() != null)
				{
					errmsg.append(String.format("\t {%s}", ps.getValue()));
				}
			}
			errmsg.append("]");
		}
		return errmsg.toString();
	}

	public static void main(String[] args) throws SMException
	{
		String val = "";

		ConfigHandler.getInstance("config.xml");

		DBHelper libeka = new DBHelper();
		libeka.setSql("select node_name from acq_node where node = ?");
		libeka.appendParameter("20130626-66", Types.VARCHAR);
		val = libeka.executeString();
		System.out.println("libeka value:" + val);

		NamedDBHelper dli = new NamedDBHelper("dlidb2", "");
		dli.setSql("select title_sort from c_info where c_id = 263495");
		val = dli.executeString();
		System.out.println("DLi value:" + val);

		ExampleEntity e = new ExampleEntity();
		e.setNode("20130626-66");
		e.read();

		System.out.println("acq_node[20130626-66] node name is :" + e.getNodeName());

		e.setNodeName("견적2013");
		e.update();
		
		
		String str = "abcde";
		System.out.println(str.charAt(0));

	}
}
